#include <Python.h>
#include <pygobject.h>
#include <pygtk/pygtk.h>
#include <gtk/gtk.h>
#include <gdk/gdk.h>
#include <gdk/gdkx.h>

#include <X11/Xlib.h>
#include <X11/extensions/XTest.h>
#include <X11/extensions/XInput.h>


int parse_gdk_window(PyObject *object, gpointer address);

enum
{
	INPUTEVENT_BTN_PRESS,
	INPUTEVENT_BTN_RELEASE,
	INPUTEVENT_MOTION_NOTIFY,
	INPUTEVENT_MAX
};
	
int gnInputEvent[INPUTEVENT_MAX] = { 0 };
int nEventListCnt = 0;
XEventClass eventList[32];
XEventClass cls;
XDeviceInfoPtr devList = NULL;
int DevListCnt = 0;
XDevice* dev = NULL;

int setup = 0;
Display* display = NULL;
char* deviceName = NULL;



int setup_tablet(XID window) {
    int i;
    devList = (XDeviceInfoPtr) XListInputDevices(display, &DevListCnt);
    XDeviceInfoPtr devInfo = NULL;
    if (deviceName)
        printf("tabletIO.grab_device: Looking for Pen Tablet device named '%s'...\n", deviceName);
    else
        printf("tabletIO.grab_device: Looking for Pen Tablet device...\n");
    for (i=0; i<DevListCnt; ++i)
    {
        if (deviceName) {
            if (!strcasecmp(devList[i].name, deviceName)) {
    		    devInfo = devList + i;
        		printf("tabletIO.grab_device: Device Name: %s\n", devList[i].name);                
            }
        } else {
            // if no name is specified, look for standard names
            if ( (((devList[i].name[0] == 'P') && (devList[i].name[1] == 'T')) || // Pen Tablet
                  (!strcasecmp(devList[i].name,"Wacom Pen"))  || // Wacom 
                  (!strcasecmp(devList[i].name,"stylus"))
                 ) && devList[i].num_classes ) {
                    if ((devInfo == NULL)  || (strcasecmp(devInfo->name,"Wacom Pen"))) {
                        // if multiple devices found, prefer "Wacom Pen"
            		    devInfo = devList + i;
                		printf("tabletIO.grab_device: Device Name: %s\n", devList[i].name);
            		}
            	}
        }
    }
    if (!devInfo)
	{
		fprintf(stderr,"tabletIO.grab_device: No pen tablet input device found.\n");
		return 0;
	}
    dev = XOpenDevice(display,devInfo->id);
    // TODO register exit handler to XCloseDevice() the dev !?
    XFreeDeviceList(devList); devList = NULL;
	if (!dev)
	{
		fprintf(stderr,"tabletIO.grab_device: Unable to open input device.\n");
		return 0;
	}

	// button events
	DeviceButtonPress(dev,gnInputEvent[INPUTEVENT_BTN_PRESS],cls);
	if (cls) eventList[nEventListCnt++] = cls;
	DeviceButtonRelease(dev,gnInputEvent[INPUTEVENT_BTN_RELEASE],cls);
	if (cls) eventList[nEventListCnt++] = cls;

	// motion events
	DeviceMotionNotify(dev,gnInputEvent[INPUTEVENT_MOTION_NOTIFY],cls);
	if (cls) eventList[nEventListCnt++] = cls;
	return 1;
}

/*
 * tabletIO.grab_device(window, device)
 *
 * Grabs the input of pen tablet 'device' for exclusive use by 'window'
 *
 * GdkWindow window:  The window which will receive the input
 * String    device:  The name of the device to be grabbed 
 */
static PyObject * grab_device(PyObject *self, PyObject *args) {
    PyObject* obj;
    GdkWindow* window = NULL;

    deviceName = NULL;
    if (!PyArg_ParseTuple(args, "O|s", &obj, &deviceName))
        return NULL;
    parse_gdk_window(obj, &window);
    if (window == NULL)
        return NULL;
    
    //asserts that grab_device() is called before send_key()
    display = gdk_x11_drawable_get_xdisplay(window);
    XID xwin = gdk_x11_drawable_get_xid(window);
    if (!setup) {
        if ( setup_tablet(xwin) ) {
            setup = 1;
        } else {
            Py_RETURN_NONE;
        }
    }
    int err = XGrabDevice(display,dev,xwin,
		0, // no owner events
		nEventListCnt, eventList, // events
		GrabModeAsync, // don't queue, give me whatever you got
		GrabModeAsync,
		CurrentTime);
    if (err == AlreadyGrabbed)
        fprintf(stderr, "Error: tabletIO.grab_device: AlreadyGrabbed\n");
    else if (err == GrabNotViewable)
        fprintf(stderr, "Error: tabletIO.grab_device: GrabNotViewable\n");
    else if (err == GrabFrozen)
        fprintf(stderr, "Error: tabletIO.grab_device: GrabFrozen\n");
    
    return Py_BuildValue("i", err);
}



static KeyCode modifier_keycodes[8] = {0,0,0,0,0,0,0,0};
static int find_modifier_keycode(Display *display, int modmap_index, KeyCode *code_ret) {
  XModifierKeymap *map;
  int key;
  KeyCode keycode;

  map = XGetModifierMapping(display);
  for (key=0; key < map->max_keypermod; key++) {
      keycode = map->modifiermap[modmap_index * map->max_keypermod + key]; 
      if (keycode != 0) {
	*code_ret = keycode;
	XFreeModifiermap(map);
	return 0;
      }
  }
  XFreeModifiermap(map);
  return 1;
}

/*
 * tabletIO.send_key(keycode, modifier_state)
 *
 * Simulates a press of the key with optional modifier keys like Shift held down.
 * WARNING: Before this function may be used the first time, tabletIO.grab_device()
 * has to be called once to ensure the 'display' variable is set to a valid output
 * display.
 *
 * int keycode:         the KeyCode of the key to be simulated
 * int modifier_state:  bit map of modifier keys to be used
 */
static PyObject * send_key(PyObject *self, PyObject *args) {
    unsigned char keycode, modifier_state;

    if (!PyArg_ParseTuple(args, "BB", &keycode, &modifier_state))
        return NULL;

    if (!display) {
        PyErr_SetString(PyExc_AttributeError, "internal var display is not set, run "
                        "tabletIO.grab_device(wnd) with a valid GdkWindow wnd first.");
        return 0;
    }
    //printf("keycode %d, modifiers 0x%x\n", keycode, modifier_state);

    // Go through all modifiers and press those which should be on
    int i;
    for (i = 0; i < 8; ++i) {
        if (modifier_state & (1<<i)) {      // modifier #i is on
            if (modifier_keycodes[i] == 0) {
                KeyCode kc = 0;
                find_modifier_keycode(display, i, &kc);
                modifier_keycodes[i] = kc;
            }
            //printf("Modifier #%d is on, retrieved keycode %d\n", i, modifier_keycodes[i]);
            if (modifier_keycodes[i]) {
                //printf("Pressing keycode %d\n", modifier_keycodes[i]);
                XTestFakeKeyEvent(display, modifier_keycodes[i], 1, CurrentTime);
            }
        }
    }
    
    // press and release actual key
    XTestFakeKeyEvent( display, keycode, 1, CurrentTime );
    XTestFakeKeyEvent( display, keycode, 0, CurrentTime );
    
    // release all modifiers again
    for (i = 0; i < 8; ++i) {
        if (modifier_state & (1<<i)) {      // modifier #i is on
            // modifier_keycodes[i]  already filled here
            if (modifier_keycodes[i]) {
                //printf("Releasing keycode %d\n", modifier_keycodes[i]);
                XTestFakeKeyEvent(display, modifier_keycodes[i], 0, CurrentTime);
            }
        }
    }
    
    Py_RETURN_NONE;
}


static PyMethodDef TabletIOMethods[] = {
    {"grab_device",  grab_device, METH_VARARGS,
     "Grab input from tablet device to specified GdkWindow"},
    {"send_key",  send_key, METH_VARARGS,
     "Send a key event."},
    {NULL, NULL, 0, NULL}        // Sentinel
};

PyMODINIT_FUNC
inittabletIO(void)
{
    init_pygobject();
    init_pygtk();
    PyObject* m;
    m = Py_InitModule("tabletIO", TabletIOMethods);
}

PyTypeObject * gdesklets_get_pygobject_type(void)
{
  static PyTypeObject *PyGObject_Type = NULL;

  if(G_UNLIKELY(PyGObject_Type == NULL))
    {
      PyObject *module;

      module = PyImport_ImportModule("gobject");

      if(!module)
        goto err_gobject;

      PyObject *moddict = PyModule_GetDict(module);

      PyGObject_Type = (PyTypeObject *) PyDict_GetItemString(moddict, "GObject");

      if (PyGObject_Type == NULL)
        goto err_gobject;
    }

  return PyGObject_Type;

 err_gobject:
  PyErr_SetString(PyExc_ImportError, "cannot import name GObject from gobject");
  return NULL;
}



int parse_gdk_window(PyObject *object, gpointer address)
{
  GdkWindow **window = address;

  if(!pygobject_check(object, gdesklets_get_pygobject_type()))
    goto err;

  if(!GDK_IS_WINDOW(pygobject_get(object)))
    goto err;

  *window = GDK_WINDOW(pygobject_get(object));

  return 1;

 err:
  PyErr_SetString(PyExc_TypeError, "first parameter must be a GdkWindow");
  return 0;
}




